home
***
CD-ROM
|
disk
|
other
***
search
/
SGI Hot Mix 14
/
Hot Mix 14.iso
/
.all
/
bin
/
Game
/
SlidePuzzle.java
(
.txt
)
< prev
Wrap
Text File
|
1996-07-23
|
23KB
|
717 lines
import java.awt.*;
import java.applet.*;
import java.awt.image.*;
/**
* The definitive (at least until I write version 2.0) Slide Puzzle
* class. Allows for specification of the image in the slide puzzle,
* the size (number of rows and columns), as well as the sound to use
* when scrambling the puzzle, when moving tiles, and when the puzzle
* is solved. Fully animated tile slides, and allows multiple tiles
* to be slid on one mouse click.
*<pre>
* Revision History
* 1.2 -- Changed puzzle to extend Canvas to make it a component
* that can be used anywhere.
* 1.1 -- Added support for different images, sounds, and different
* puzzle sizes.
* 1.0 -- Initial version, required a graphics context to be passed
* in for drawing.
*</pre>
* @author Andrew Wack
* @version 1.2 May 8, 1996
*/
class SlidePuzzle extends Canvas implements Runnable {
/*
* Sounds to play when scrambling, moving tiles, or when puzzle is solved.
*/
private AudioClip scrambledMusic;
private AudioClip solvedMusic;
private AudioClip moveMusic;
/*
* Image to use for puzzle (does not include border).
*/
private Image tileImage;
/*
* Entire puzzle image (including borders) as currently being
* displayed on the screen.
*/
private Image puzImage = null;
/*
* Sizes of various parts of the puzzle.
*/
private int numCols;
private int numRows;
private Dimension tileSize;
private Dimension borderSize = new Dimension(5,5);
private static final Dimension minBorder = new Dimension(0,0);
/*
* Width of "cut" between tiles.
*/
private int cutWidth = 0;
/*
* Number of steps to use when animating tile move
*/
private int animSteps;
/*
* Array to keep track of the current position of each tile.
* When solved, the numbers in this array are in order across the
* rows. For programming convenience, this array has a border of
* tiles that are not part of the puzzle. For example, in a
* 4 x 4 puzzle the array would look like:
*<pre>
* 0 1 2 3 4 5
* 6 7 8 9 10 11
* 12 13 14 15 16 17
* 18 19 20 21 22 23
* 24 25 26 27 28 29
* 30 31 32 33 34 35
*</pre>
*/
private int[][] tilePos;
/*
* Tile number for the blank tile (space in puzzle) and the current
* row and column location of the blank tile.
*/
private int blankTileNum;
private int blankRow;
private int blankCol;
/*
* The following variables indicate to the animator which direction
* to shift, and how many tiles are being shifted.
* Only one of colShiftDir and rowShiftDir can be non-zero once the
* animator is called.
*/
private int colShiftDir;
private int rowShiftDir;
private int numColShift;
private int numRowShift;
/*
* The physical coordinates of the upper left hand corner of the
* tiles to move, along with their destination coordinates.
*/
private Point moveFrom = new Point(0,0);
private Point moveTo = new Point(0,0);
/*
* Variables used to indicate the current state of the puzzle, or
* actions taking place.
*/
private boolean solved = true;
private boolean animating = false;
private boolean doScramble = false;
/*
* Thread for animation.
*/
private Thread animator = null;
/*
* MediaTracker object used to load user supplied tile image.
*/
private MediaTracker tracker;
/**
* Creates a slide puzzle component.
*
* @param Cols number of columns in puzzle
* @param Rows number of rows in puzzle
* @param Image image to be used for the tile area of the puzzle
* @param blankRow row number where the empty space should appear
* in puzzle (first row is 1)
* @param blankCol col number where the empty space should appear
* in puzzle (first col is 1)
* @param cutWidth amount of space between puzzle pieces (can be 0)
* @param scramble music to play while scrambling puzzle
* @param solved music to play when puzzle is solved
* @param move music to play when moving tiles
*/
public SlidePuzzle(int Rows, int Cols, Image tileImage, int blankRow,
int blankCol, int cutWidth,
AudioClip scramble, AudioClip solved, AudioClip move) {
scrambledMusic = scramble;
solvedMusic = solved;
moveMusic = move;
this.tileImage = tileImage;
/*
* Start loading image
*/
tracker = new MediaTracker(this);
tracker.addImage(this.tileImage, 0);
numCols = Cols;
numRows = Rows;
if (cutWidth > 0)
this.cutWidth = cutWidth;
/*
* Initialize tile position array
*/
tilePos = new int[numRows+2][numCols+2];
for (int col = 0; col <= numCols+1; col++)
for (int row = 0; row <= numRows+1; row++)
tilePos[row][col] = row*(numCols+2) + col;
/*
* Check to make sure we have legal blankRow and blankCol
* arguments. If so, copy them to local, otherwise use
* defaults.
*/
if ((blankRow < 1) || (blankRow > Rows))
this.blankRow = Rows;
else
this.blankRow = blankRow;
if ((blankCol < 1) || (blankCol > Cols))
this.blankCol = Cols;
else
this.blankCol = blankCol;
blankTileNum = this.blankRow * (numCols+2) + this.blankCol;
/*
* We need to know the size of the image, and have it loaded
* before we are done with the constructor
*/
try {
tracker.waitForID(0);
} catch (InterruptedException e) { }
tileSize = new Dimension((tileImage.getWidth(null) + cutWidth +
numCols - 1) / numCols,
(tileImage.getHeight(null) + cutWidth +
numRows - 1) / numRows);
resize(preferredSize());
/*
* Start up thread for animation.
*/
//start();
}
/**
* Layout takes care of creating the puzImage from the tileImage
* if the puzzle had not been laid out yet. Creates
* the borders and puts in lines between the tiles. If the
* puzzle had already been laid out, then this must be a resize
* request, so we copy from the existing puzImage.
*/
public void layout() {
super.layout();
Dimension d = size();
Image oldPuz = puzImage;
Dimension oldBorderSize = borderSize;
puzImage = createImage(d.width, d.height);
Graphics g = puzImage.getGraphics();
g.setColor(getBackground());
g.fillRect(0,0,d.width, d.height);
g.setColor(getForeground());
/*
* Compute the border size and draw a nice 3d border around the
* tiles. The border fills any leftover area between the tile
* image size and the actual size of the component.
*/
borderSize = new Dimension ((d.width - numCols*tileSize.width -
cutWidth) / 2,
(d.height - numRows*tileSize.height -
cutWidth) / 2);
Color lightBorder = getForeground().brighter().brighter();
Color darkBorder = getForeground().darker().darker();
int end = Math.max(borderSize.width, borderSize.height);
for (int i = 0; i < end; i++) {
if (i < end * 3 / 4) {
g.setColor(lightBorder);
} else {
g.setColor(darkBorder);
}
g.drawLine(i * borderSize.width / end, i * borderSize.height / end,
i * borderSize.width / end,
d.height - i * borderSize.height / end);
g.drawLine(i * borderSize.width / end, i * borderSize.height / end,
d.width - i * borderSize.width / end,
i * borderSize.height / end);
if (i >= end * 3 / 4) {
g.setColor(lightBorder);
} else {
g.setColor(darkBorder);
}
g.drawLine(d.width - i * borderSize.width / end,
i * borderSize.height / end,
d.width - i * borderSize.width / end,
d.height - i * borderSize.height / end);
g.drawLine(i * borderSize.width / end,
d.height - i * borderSize.height / end,
d.width - i * borderSize.width / end,
d.height - i * borderSize.height / end);
}
/*
* If this is the first time in layout (oldPuz == null), then we
* draw in the tileImage, and then put in the cut lines between the
* tiles. Otherwise copy old image of puzzle to new image.
*/
if (oldPuz == null) {
g.drawImage(tileImage, (d.width - tileImage.getWidth(null)) / 2,
(d.height - tileImage.getHeight(null)) / 2, this);
g.setColor(getBackground());
for (int i = 0; i <= numCols; i++)
for (int j = 0; j < cutWidth; j++)
g.drawLine(i * tileSize.width + borderSize.width + j,
borderSize.height,
i * tileSize.width + borderSize.width + j,
d.height - borderSize.height - 1);
for (int i = 0; i <= numRows; i++)
for (int j = 0; j < cutWidth; j++)
g.drawLine(borderSize.width,
i * tileSize.height + borderSize.height + j,
d.width - borderSize.width - 1,
i * tileSize.height + borderSize.height + j);
g.fillRect((blankCol - 1) * tileSize.width + borderSize.width + cutWidth,
(blankRow - 1) * tileSize.height + borderSize.height +
cutWidth, tileSize.width, tileSize.height);
} else {
g.clipRect(borderSize.width, borderSize.height,
tileSize.width * numCols + cutWidth,
tileSize.height * numRows + cutWidth);
g.drawImage(oldPuz, borderSize.width - oldBorderSize.width,
borderSize.height - oldBorderSize.height, this);
}
g.dispose();
start();
}
/**
* Preferred size of the component.
*/
public Dimension preferredSize() {
return minimumSize();
}
/**
* Minimum size of the component. Minimum size is just size of the tiles
* plus the size of the smallest border.
*/
public Dimension minimumSize() {
return new Dimension(tileSize.width * numCols + (numCols + 1) * cutWidth +
2 * minBorder.width,
tileSize.height * numRows + (numRows + 1) * cutWidth
+ 2 * minBorder.height);
}
/**
* Resize the component. We never allow the component to be shrunk
* smaller than minimumSize, since we have no way of effectively resizing
* the user supplied tile image.
*
* @see #minimumSize
*/
public synchronized void reshape (int x, int y, int width, int height) {
Dimension d = minimumSize();
width = Math.max(width, d.width);
height = Math.max(height, d.height);
super.reshape(x, y, width, height);
}
/**
* Start the animator thread
*/
public void start() {
animator = new Thread(this);
doScramble = true;
animator.start();
}
/**
* Stop the animator thread
*/
public void stop() {
if (animator != null) animator.stop();
animator = null;
}
/**
* Check to see if the puzzle is currently in a solved state.
*
* @returns true if puzzle is solved, false otherwise.
*/
private boolean checkSolution() {
for (int col = 1; col <= numCols; col++)
for (int row = 1; row <= numRows; row++)
if (tilePos[row][col] != row * (numCols + 2) + col)
return false;
return true;
}
/**
* Handle mouse down event.
*/
public boolean mouseDown(Event e, int x, int y) {
/*
* Convert x and y to coordinates within the tile images.
* and find the row and column number of tile that has been clicked
* on.
*/
x = x - borderSize.width - cutWidth;
y = y - borderSize.height - cutWidth;
int tileRow = (y / tileSize.height) + 1;
int tileCol = (x / tileSize.width) + 1;
/*
* If user clicked on one of the tiles, (not border)
* and if we are not currently doing animation, then we
* process mouse event, otherwise ignore it.
*/
if ((tileRow >= 1) && (tileRow <= numRows) &&
(tileCol >= 1) && (tileCol <= numCols) && !animating) {
/*
* If the puzzle is already solved, then scramble the puzzle.
* Set flags, and wake up the animator.
*/
if (solved && !doScramble) {
doScramble = true;
animating = true;
animator.resume();
} else {
/*
* Potential tile move situation. Compute how far away the
* empty slot in puzzle is relative to where the mouse down
* event occurred.
*/
numColShift = blankCol - tileCol;
numRowShift = blankRow - tileRow;
/*
* Its a legal move only if the blank is in the same row or
* or same column of the tile that was clicked on.
*/
if (((numColShift == 0) || (numRowShift == 0)) &&
(numColShift != numRowShift)) {
/*
* Set up shifting variables appropriately depending on
* whether we are shifting a row of tiles, or a column of
* tiles. Also set up the animSteps variable since we know
* at this point whether we are shifting rows or columns.
*/
if (numColShift == 0) {
numColShift = 1;
colShiftDir = 0;
rowShiftDir = numRowShift / Math.abs(numRowShift);
numRowShift = Math.abs(numRowShift);
animSteps = Math.min(Math.max(3, tileSize.height / 10), 10);
} else {
numRowShift = 1;
rowShiftDir = 0;
colShiftDir = numColShift / Math.abs(numColShift);
numColShift = Math.abs(numColShift);
animSteps = Math.min(Math.max(3, tileSize.width / 10), 10);
}
/*
* Depending on which of the four directions we are
* shifting, set up the moveFrom, moveTo variables,
* fix up the tile positions in the puzzle, and fix up the
* position of the blank tile.
*/
if (colShiftDir == 1) {
moveFrom.move(borderSize.width + cutWidth +
(tileCol - 1) * tileSize.width,
borderSize.height + cutWidth +
(tileRow - 1) * tileSize.height);
moveTo.move(moveFrom.x + tileSize.width, moveFrom.y);
for (int i = tileCol + numColShift; i > tileCol; i--)
tilePos[tileRow][i] = tilePos[tileRow][i - 1];
tilePos[tileRow][tileCol] = blankTileNum;
blankRow = tileRow;
blankCol = tileCol;
} else if (colShiftDir == -1) {
tileCol = tileCol - numColShift + 1;
moveFrom.move(borderSize.width + cutWidth +
(tileCol - 1) * tileSize.width,
borderSize.height + cutWidth +
(tileRow - 1) * tileSize.height);
moveTo.move(moveFrom.x - tileSize.width, moveFrom.y);
for (int i = tileCol; i < tileCol+numColShift; i++)
tilePos[tileRow][i-1] = tilePos[tileRow][i];
tilePos[tileRow][tileCol + numColShift - 1] = blankTileNum;
blankRow = tileRow;
blankCol = tileCol + numColShift - 1;
} else if (rowShiftDir == 1) {
moveFrom.move(borderSize.width + cutWidth +
(tileCol - 1) * tileSize.width,
borderSize.height + cutWidth +
(tileRow - 1) * tileSize.height);
moveTo.move(moveFrom.x, moveFrom.y + tileSize.height);
for (int i = tileRow + numRowShift; i > tileRow; i--)
tilePos[i][tileCol] = tilePos[i - 1][tileCol];
tilePos[tileRow][tileCol] = blankTileNum;
blankRow = tileRow;
blankCol = tileCol;
} else if (rowShiftDir == -1) {
tileRow = tileRow - numRowShift + 1;
moveFrom.move(borderSize.width + cutWidth +
(tileCol - 1) * tileSize.width,
borderSize.height + cutWidth +
(tileRow - 1) * tileSize.height);
moveTo.move(moveFrom.x, moveFrom.y - tileSize.height);
for (int i = tileRow; i < tileRow+numRowShift; i++)
tilePos[i-1][tileCol] = tilePos[i][tileCol];
tilePos[tileRow + numRowShift - 1][tileCol] = blankTileNum;
blankRow = tileRow + numRowShift - 1;
blankCol = tileCol;
}
/*
* Check to see if the puzzle is solved, and then wake up
* animator to do tile move.
*/
solved = checkSolution();
animating = true;
animator.resume();
}
}
return true;
}
return super.mouseDown(e, x, y);
}
/*
* Animator thread. Sits in a suspended state, until awoken by the mouse
* down event handler. Then it checks the state variables to see if it
* should scramble the puzzle or do a move.
*/
public void run() {
while (true) {
if (doScramble)
scramblePuzzle();
else
doMove();
animating = false;
animator.suspend();
}
}
/**
* Perform a sequence of random moves to scramble the puzzle.
*/
private void scramblePuzzle() {
int rowOffset, colOffset;
int row, col;
Graphics myG;
if (scrambledMusic != null)
scrambledMusic.play();
Graphics pg = puzImage.getGraphics();
pg.setColor(getBackground());
/*
* Randomize the puzzle. Wait between each move
* a small amount of time. We redraw after each move so the user
* can "follow" the puzzle being scrambled :-)
*/
int numMoves = Math.max(200, numRows * numCols * numRows * numCols);
for (int i = 0; i < numMoves; i++) {
do
rowOffset = (int)(Math.random() * 3) - 1;
while (rowOffset > 1);
do
colOffset = (int)(Math.random() * 3) - 1;
while (colOffset > 1);
if (!((rowOffset != 0) && (colOffset != 0)) &&
(blankCol + colOffset > 0) && (blankCol + colOffset <= numCols) &&
(blankRow + rowOffset > 0) && (blankRow + rowOffset <= numRows)) {
row = blankRow + rowOffset;
col = blankCol + colOffset;
pg.copyArea(borderSize.width + cutWidth + (col - 1) * tileSize.width,
borderSize.height + cutWidth + (row - 1) * tileSize.height,
tileSize.width, tileSize.height,
(blankCol - col) * tileSize.width,
(blankRow - row) * tileSize.height);
pg.fillRect(borderSize.width + cutWidth + (col - 1) * tileSize.width,
borderSize.height + cutWidth + (row - 1) * tileSize.height,
tileSize.width, tileSize.height);
tilePos[blankRow][blankCol] = tilePos[row][col];
tilePos[row][col] = blankTileNum;
blankCol = col;
blankRow = row;
//myG = getGraphics();
//myG.clipRect(borderSize.width + cutWidth + tileSize.width *
// ((blankCol < col? blankCol: col) - 1),
// borderSize.height + cutWidth + tileSize.height *
// ((blankRow < row? blankRow: row) - 1),
// (Math.abs(colOffset) + 1) * tileSize.width,
// (Math.abs(rowOffset) + 1) * tileSize.height);
//paint(myG);
//myG.dispose();
}
}
paint(getGraphics());
pg.dispose();
solved = false;
doScramble = false;
}
/**
* Do an animated move of the tiles in the puzzle.
* All drawing is done in the offscreen image puzImage, then copied
* to the main screen with appropriate setting of the clipping rectangle.
*/
private void doMove() {
Point myStart = new Point(moveFrom.x, moveFrom.y);
Rectangle clip1, clip2;
Graphics pg;
Graphics myG;
Point prevFrom = new Point(0,0);
if (moveMusic != null)
moveMusic.play();
/*
* We do animSteps number of steps to move the tiles.
*/
for (int moveStep = 1; moveStep <= animSteps; moveStep++) {
/*
* The first clipping rectangle is just the are that we are moving
* from.
*/
clip1 = new Rectangle(moveFrom.x, moveFrom.y,
tileSize.width * numColShift, tileSize.height * numRowShift);
/*
* Copy old move from location to prevFrom, and reset moveFrom to
* original start position. This makes it easier to calculate
* the next moveFrom position.
*/
prevFrom.move(moveFrom.x, moveFrom.y);
moveFrom.move(myStart.x, myStart.y);
/*
* Calculate where we should move to on this animation step.
*/
if (moveStep == animSteps)
moveFrom.move(moveTo.x, moveTo.y);
else
moveFrom.translate(moveStep * colShiftDir * tileSize.width / animSteps,
moveStep * rowShiftDir * tileSize.height / animSteps);
/*
* The overall clipping region is the union of the old moveFrom and
* the new moveFrom
*/
clip2 = clip1.union(new Rectangle(moveFrom.x, moveFrom.y,
tileSize.width * numColShift,
tileSize.height * numRowShift));
/*
* Do the actual image move on the puzImage.
*/
pg = puzImage.getGraphics();
pg.clipRect(clip2.x, clip2.y, clip2.width, clip2.height);
pg.copyArea(prevFrom.x, prevFrom.y, tileSize.width * numColShift,
tileSize.height * numRowShift, moveFrom.x - prevFrom.x,
moveFrom.y - prevFrom.y);
/*
* Fill in with background color where the new blank area appears.
*/
pg.setColor(getBackground());
if (colShiftDir == 1)
pg.fillRect(prevFrom.x, prevFrom.y, moveFrom.x - prevFrom.x,
tileSize.height);
else if (colShiftDir == -1)
pg.fillRect(moveFrom.x + (tileSize.width * numColShift),
prevFrom.y, prevFrom.x - moveFrom.x, tileSize.height);
else if (rowShiftDir == 1)
pg.fillRect(prevFrom.x, prevFrom.y, tileSize.width,
moveFrom.y - prevFrom.y);
else if (rowShiftDir == -1)
pg.fillRect(prevFrom.x, moveFrom.y + tileSize.height * numRowShift,
tileSize.width, prevFrom.y - moveFrom.y );
pg.dispose();
/*
* Copy the changed image data to the screen.
*/
myG = getGraphics();
myG.clipRect(clip2.x, clip2.y, clip2.width, clip2.height);
myG.drawImage(puzImage, 0, 0, this);
myG.dispose();
/*
* If we are done and solved, play music. Otherwise, if not done
* sleep for 1/10 second and then do next animation step.
*/
if (moveStep == animSteps) {
if ((solved) && (solvedMusic != null))
solvedMusic.play();
} else {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
break;
}
}
}
}
/**
* Since our puzzle covers the entire area of the component, no
* need to clear the background before calling paint.
*/
public void update(Graphics g) {
paint(g);
}
/*
* Make sure the slide puzzle has been validated before we attempt to
* paint it!
*/
public void paint(Graphics g) {
if (!isValid()) validate();
g.drawImage(puzImage, 0,0, this);
}
}